package ee.objectCloner.impl;

import java.lang.reflect.Array;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URI;
import java.net.URL;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Pattern;

import ee.objectCloner.CloneAdvisor;
import ee.objectCloner.CloneAdvisor.CloneAdvise;
import ee.objectCloner.CustomCloner;
import ee.objectCloner.ObjectCloner;
import ee.objectCloner.impl.cloners.ArrayListCloner;
import ee.objectCloner.impl.cloners.HashMapCloner;

//TODO create tests and comments
public class ObjectClonerImpl implements ObjectCloner {

	private Set<Class<?>> _immutables;
	private Set<CustomCloner> _customCloners;
	private Map<TypeKey, CustomCloner> _customClonerCache;
	private Set<CloneAdvisor> _cloneAdvisors;
	private Map<TypeKey, CloneAdvisor> _cloneAdvisorCache;
	private CustomCloner _defaultCloner;

	public ObjectClonerImpl(CustomCloner defaultCloner) {
		_defaultCloner = defaultCloner;
		_immutables = new HashSet<Class<?>>();
		_customCloners = new HashSet<CustomCloner>();
		_customClonerCache = new HashMap<TypeKey, CustomCloner>();
		_cloneAdvisors = new HashSet<CloneAdvisor>();
		_cloneAdvisorCache = new HashMap<TypeKey, CloneAdvisor>();

		addDefaultImmutables();
		addDefaultCustomCloners();
	}

	protected void addDefaultCustomCloners() {
		addCustomCloner(new HashMapCloner());
		addCustomCloner(new ArrayListCloner());
	}

	protected void addDefaultImmutables() {
		addImmutable(Boolean.class);
		addImmutable(Character.class);
		addImmutable(Byte.class);
		addImmutable(Short.class);
		addImmutable(Integer.class);
		addImmutable(Long.class);
		addImmutable(Float.class);
		addImmutable(Double.class);
		addImmutable(Void.class);

		addImmutable(String.class);
		addImmutable(Class.class);
		addImmutable(BigDecimal.class);
		addImmutable(BigInteger.class);
		addImmutable(URI.class);
		addImmutable(URL.class);
		addImmutable(UUID.class);
		addImmutable(Pattern.class);
	}

	@Override
	public void addCustomCloner(CustomCloner customCloner) {
		_customCloners.add(customCloner);
	}

	@Override
	public void addImmutable(Class<?> type) {
		_immutables.add(type);
	}

	@Override
	public void addCloneAdvisor(CloneAdvisor cloneAdvisor) {
		_cloneAdvisors.add(cloneAdvisor);
	}

	@Override
	@SuppressWarnings("unchecked")
	public <T> T clone(T source) {
		if (source == null) {
			return null;
		}

		Class<?> sourceType = source.getClass();

		if (_immutables.contains(sourceType)) {
			return source;
		}

		CloneAdvise cloneAdvise = _getCloneAdvise(source);

		switch (cloneAdvise) {
		case CLONE: /* proceed */
			break;
		case USE_SOURCE:
			return (T) source;
		case COPY:
			throw new RuntimeException(
					"Could not handle CloneAdvise.COPY from the clone method, use copyOrClone instead");
		default:
			throw new RuntimeException("Could not handle CloneAdvise: "
					+ cloneAdvise);
		}

		CustomCloner customCloner = _getCustomCloner(sourceType);
		if (customCloner != null) {
			return (T) customCloner.clone(source, this);
		}

		if (sourceType.isEnum()) {
			return source;
		}

		if (sourceType.isArray()) {
			Object[] sourceArray = (Object[]) source;
			int length = sourceArray.length;
			Object[] destination = (Object[]) Array.newInstance(
					sourceType.getComponentType(), length);

			for (int i = 0; i < length; i++) {
				destination[i] = clone(sourceArray[i]);
			}

			return (T) destination;
		}

		return (T) _defaultCloner.clone(source, this);
	}

	@Override
	/**
	 * Calls copy(source, destination, false) 
	 */
	public void copy(Object source, Object destination) {
		copy(source, destination, false);
	}

	/**
	 * Does not use registered CloneAdvisors, if you think about it, it would
	 * not make any sense
	 */
	public <T> void copy(Object source, T destination,
			boolean allowCustomCloners) {
		if (source == null) {
			throw new RuntimeException("Can not copy null into a destination");
		}

		if (destination == null) {
			throw new RuntimeException("Can not copy into null");
		}

		Class<?> sourceType = source.getClass();
		Class<?> destinationType = destination.getClass();

		if (_immutables.contains(sourceType)) {
			throw new RuntimeException(
					"Can not copy immutables into a destination");
		}

		if (allowCustomCloners) {
			CustomCloner customCloner = _getCustomCloner(sourceType,
					destinationType);

			if (customCloner != null) {
				customCloner.copy(source, destination, this);
			}
		}

		if (sourceType.isEnum()) {
			throw new RuntimeException("Can not copy enums into a destination");
		}

		if (sourceType.isArray()) {
			Object[] sourceArray = (Object[]) source;
			Object[] destinationArray = (Object[]) destination;

			int length = sourceArray.length;

			if (length != destinationArray.length) {
				throw new RuntimeException(
						"Can only copy into arrays if they have the same length");
			}

			for (int i = 0; i < length; i++) {
				destinationArray[i] = copyOrClone(sourceArray[i],
						destinationArray[i]);
			}
		}

		_defaultCloner.copy(source, destination, this);
	}

	@SuppressWarnings("unchecked")
	@Override
	public <T> T copyOrClone(Object source, T destination) {
		if (source == null) {
			return null;
		}

		if (destination == null) {
			return (T) clone(source);
		}

		Class<?> sourceType = source.getClass();
		Class<?> destinationType = destination.getClass();

		if (_immutables.contains(sourceType)
				&& destinationType.isAssignableFrom(sourceType)) {
			return (T) source;
		}

		CloneAdvise cloneAdvise = _getCloneAdvise(source, destination);

		switch (cloneAdvise) {
		case CLONE:
			return (T) clone(source);
		case USE_SOURCE:
			return (T) source;
		case COPY: /* proceed */
			break;
		default:
			throw new RuntimeException("Could not handle CloneAdvise: "
					+ cloneAdvise);
		}

		CustomCloner customCloner = _getCustomCloner(sourceType,
				destinationType);
		if (customCloner != null) {
			customCloner.copy(source, destination, this);
			return destination;
		}

		if (sourceType.isEnum()) {
			return (T) source;
		}

		if (sourceType.isArray()) {
			Object[] sourceArray = (Object[]) source;
			Object[] destinationArray = (Object[]) destination;

			int length = sourceArray.length;

			if (length != destinationArray.length) {
				destinationArray = Arrays.copyOf(destinationArray, length);
			}

			for (int i = 0; i < length; i++) {
				destinationArray[i] = copyOrClone(sourceArray[i],
						destinationArray[i]);
			}
			return (T) destinationArray;
		}

		_defaultCloner.copy(source, destination, this);

		return destination;
	}

	private CloneAdvise _getCloneAdvise(Object source, Object destination) {
		Class<?> sourceType = source.getClass();
		Class<?> destinationType = destination.getClass();
		TypeKey key = new TypeKey(sourceType);
		
		CloneAdvisor foundCloneAdvisor = null;
		
		if (_cloneAdvisorCache.containsKey(key)) {
			foundCloneAdvisor = _cloneAdvisorCache.get(key);
		} else {
			
			for (CloneAdvisor cloneAdvisor : _cloneAdvisors) {
				if (cloneAdvisor.canAdvise(sourceType, destinationType)) {
					foundCloneAdvisor = cloneAdvisor;
					break;
				}
			}
			
			_cloneAdvisorCache.put(key, foundCloneAdvisor);
		}
		if (foundCloneAdvisor == null) {
			return CloneAdvise.COPY;
		} else {
			return foundCloneAdvisor.getAdvise(source);
		}
		
	}
	
	private CloneAdvise _getCloneAdvise(Object source) {
		Class<?> sourceType = source.getClass();
		TypeKey key = new TypeKey(sourceType);

		CloneAdvisor foundCloneAdvisor = null;
		
		if (_cloneAdvisorCache.containsKey(key)) {
			foundCloneAdvisor = _cloneAdvisorCache.get(key);
		} else {

			for (CloneAdvisor cloneAdvisor : _cloneAdvisors) {
				if (cloneAdvisor.canAdvise(sourceType)) {
					foundCloneAdvisor = cloneAdvisor;
					break;
				}
			}

			_cloneAdvisorCache.put(key, foundCloneAdvisor);
		}
		if (foundCloneAdvisor == null) {
			return CloneAdvise.CLONE;
		} else {
			return foundCloneAdvisor.getAdvise(source);
		}

	}

	private CustomCloner _getCustomCloner(Class<?> source, Class<?> destination) {
		TypeKey key = new TypeKey(source, destination);

		if (_customClonerCache.containsKey(key)) {
			return _customClonerCache.get(key);
		} else {
			CustomCloner foundCloner = null;
			for (CustomCloner customCloner : _customCloners) {
				if (customCloner.canCopy(source, destination)) {
					foundCloner = customCloner;
					break;
				}
			}

			_customClonerCache.put(key, foundCloner);
			return foundCloner;
		}
	}

	private CustomCloner _getCustomCloner(Class<?> source) {
		TypeKey key = new TypeKey(source);

		CustomCloner foundCloner = null;

		if (_customClonerCache.containsKey(key)) {
			foundCloner = _customClonerCache.get(key);
			if (foundCloner != null && foundCloner.canClone(source)) {
				return foundCloner;
			}
			// the cloner for this type can not clone it
			return null;
		} else {

			for (CustomCloner customCloner : _customCloners) {
				if (customCloner.canClone(source)) {
					foundCloner = customCloner;
					break;
				}
			}

			_customClonerCache.put(key, foundCloner);
			return foundCloner;
		}
	}

	class TypeKey {

		private Class<?> _destination;
		private Class<?> _source;

		public TypeKey(Class<?> source) {
			this(source, null);
		}

		public TypeKey(Class<?> source, Class<?> destination) {
			_source = source;
			_destination = destination;
		}

		@Override
		public boolean equals(Object object) {
			if (object instanceof TypeKey) {
				TypeKey customClonerId = (TypeKey) object;
				return _source.equals(customClonerId._source)
						&& ((_destination != null && _destination
								.equals(customClonerId._destination)) || (_destination == null && customClonerId._destination == null));
			}
			return false;
		}
	}
}
